<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>知识图谱可视化</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
    <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.8/dist/chart.umd.min.js"></script>
    <script src="https://d3js.org/d3.v7.min.js"></script>

    <script>
        tailwind.config = {
            theme: {
                extend: {
                    colors: {
                        primary: '#165DFF',
                        secondary: '#36CFC9',
                        accent: '#722ED1',
                        dark: '#1D2129',
                        light: '#F2F3F5',
                        'neutral-100': '#F7F8FA',
                        'neutral-200': '#E5E6EB',
                        'neutral-300': '#C9CDD4',
                        'neutral-400': '#86909C',
                        'neutral-500': '#4E5969',
                    },
                    fontFamily: {
                        inter: ['Inter', 'system-ui', 'sans-serif'],
                    },
                    boxShadow: {
                        'card': '0 4px 20px rgba(0, 0, 0, 0.08)',
                        'hover': '0 8px 30px rgba(0, 0, 0, 0.12)',
                    }
                }
            }
        };
function nodeClicked(event, d) {
        event.stopPropagation();
        selectedNode = d.id;
        loadCharacterDetails(d.id);

        d3.selectAll('circle')
            .attr('r', n => n.id === d.id ? 15 : 12)
            .attr('fill', n => n.id === d.id ? '#165DFF' : '#36CFC9');

        if (window.innerWidth < 768) {
            detailPanel.classList.remove('translate-x-full');
        }
    }
    </script>

    <style type="text/tailwindcss">
        @layer utilities {
            .content-auto { content-visibility: auto; }
            .scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; }
            .scrollbar-hide::-webkit-scrollbar { display: none; }
            .node-hover { transition: all 0.3s ease; }
            .node-hover:hover { stroke: theme('colors.primary'); stroke-width: 2px; transform: scale(1.1); }
            .bg-grid {
                background-size: 20px 20px;
                background-image: 
                    linear-gradient(to right, rgba(22, 93, 255, 0.05) 1px, transparent 1px),
                    linear-gradient(to bottom, rgba(22, 93, 255, 0.05) 1px, transparent 1px);
            }
            .error-shake { animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) both; }
            @keyframes shake {
                10%, 90% { transform: translateX(-1px); }
                20%, 80% { transform: translateX(2px); }
                30%, 50%, 70% { transform: translateX(-3px); }
                40%, 60% { transform: translateX(3px); }
            }
        }
    </style>
</head>
<body class="font-inter bg-light text-dark h-screen flex flex-col overflow-hidden">
    <!-- 顶部导航栏 -->
    <header class="bg-white border-b border-neutral-200 shadow-sm z-10">
        <div class="container mx-auto px-4 py-3 flex items-center justify-between">
            <div class="flex items-center space-x-2">
                <i class="fa fa-sitemap text-primary text-2xl"></i>
                <h1 class="text-xl font-bold text-primary">知识图谱可视化系统</h1>
            </div>
            <div class="flex items-center space-x-4">
                <div class="hidden md:flex items-center space-x-2 text-neutral-500">
                    <i class="fa fa-database"></i>
                    <span id="db-status" class="text-sm">数据库连接中...</span>
                </div>
                <button id="debug-info-btn" class="p-2 rounded-full hover:bg-neutral-100 transition-colors text-neutral-500">
                    <i class="fa fa-bug"></i>
                </button>
                <button id="theme-toggle" class="p-2 rounded-full hover:bg-neutral-100 transition-colors">
                    <i class="fa fa-moon-o text-neutral-500"></i>
                </button>
            </div>
        </div>
    </header>

    <!-- 调试信息面板 -->
    <div id="debug-panel" class="fixed top-16 right-4 bg-white rounded-lg shadow-lg p-4 w-80 hidden z-50 border border-neutral-200">
        <div class="flex justify-between items-center mb-2">
            <h3 class="font-medium text-sm">调试信息</h3>
            <button id="close-debug" class="text-neutral-400 hover:text-neutral-600"><i class="fa fa-times"></i></button>
        </div>
        <div class="text-xs text-neutral-500 space-y-1 max-h-60 overflow-y-auto scrollbar-hide">
            <div>API 基础路径: <span id="api-base-path" class="font-mono">/api</span></div>
            <div>最后请求: <span id="last-request" class="font-mono">-</span></div>
            <div>最后响应: <span id="last-response" class="font-mono">-</span></div>
            <div>状态: <span id="connection-status" class="font-mono">未知</span></div>
        </div>
    </div>

    <!-- 错误提示框 -->
    <div id="error-toast" class="fixed top-16 left-1/2 transform -translate-x-1/2 bg-red-50 text-red-600 border border-red-200 rounded-lg p-4 shadow-lg hidden z-50 max-w-md">
        <div class="flex items-start">
            <i class="fa fa-exclamation-circle text-red-500 mt-0.5 mr-3"></i>
            <div class="flex-1">
                <h3 class="font-medium text-red-700" id="error-title">加载失败</h3>
                <p class="text-sm mt-1" id="error-message">无法加载数据，请稍后重试</p>
                <div class="mt-3 flex justify-end">
                    <button id="error-retry" class="text-sm bg-red-100 hover:bg-red-200 text-red-700 px-3 py-1 rounded transition-colors">重试</button>
                </div>
            </div>
        </div>
    </div>

    <!-- 主内容区 -->
    <main class="flex flex-1 overflow-hidden">
        <!-- 左侧边栏 -->
        <aside class="w-64 bg-white border-r border-neutral-200 flex flex-col h-full transition-all duration-300" id="sidebar">
            <div class="p-4 border-b border-neutral-200">
                <div class="relative">
                    <input type="text" id="character-search" placeholder="搜索人物..." class="w-full px-4 py-2 pl-10 rounded-lg border border-neutral-200 focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition-all">
                    <i class="fa fa-search absolute left-3 top-1/2 -translate-y-1/2 text-neutral-400"></i>
                </div>
                <div class="mt-3 relative">
                    <input type="text" id="semantic-search" placeholder="语义搜索..." class="w-full px-4 py-2 pl-10 rounded-lg border border-neutral-200 focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent transition-all">
                    <i class="fa fa-lightbulb-o absolute left-3 top-1/2 -translate-y-1/2 text-accent"></i>
                </div>
            </div>
            <div class="p-4 border-b border-neutral-200">
                <h3 class="text-sm font-medium text-neutral-500 mb-2">关系深度</h3>
                <div class="flex items-center space-x-2">
                    <input type="range" id="relation-depth" min="1" max="4" value="1" class="w-full h-2 bg-neutral-200 rounded-lg appearance-none cursor-pointer accent-primary">
                    <span id="depth-value" class="text-sm font-medium min-w-[2rem] text-center">1</span>
                </div>
            </div>
            <div class="flex-1 overflow-y-auto scrollbar-hide p-2">
                <h3 class="text-sm font-medium text-neutral-500 px-2 py-1">人物列表</h3>
                <div id="characters-list" class="grid grid-cols-1 gap-1 mt-1">
                    <div class="animate-pulse p-2"><div class="h-6 bg-neutral-200 rounded w-3/4"></div></div>
                    <div class="animate-pulse p-2"><div class="h-6 bg-neutral-200 rounded w-1/2"></div></div>
                    <div class="animate-pulse p-2"><div class="h-6 bg-neutral-200 rounded w-5/6"></div></div>
                </div>
            </div>
        </aside>

        <!-- 中间图谱区域 -->
        <div class="flex-1 relative bg-grid overflow-hidden" id="graph-container">
            <svg id="graph-svg" class="w-full h-full"></svg>
            <div id="graph-placeholder" class="absolute inset-0 flex flex-col items-center justify-center text-neutral-400">
                <i class="fa fa-hand-pointer-o text-4xl mb-3 animate-bounce"></i>
                <p>请从左侧选择一个人物查看关系图谱</p>
            </div>
            <div id="graph-loading" class="absolute inset-0 flex items-center justify-center bg-light/80 backdrop-blur-sm hidden">
                <div class="flex flex-col items-center">
                    <div class="w-12 h-12 border-4 border-primary/30 border-t-primary rounded-full animate-spin"></div>
                    <p class="mt-3 text-neutral-500">加载中...</p>
                </div>
            </div>
            <div id="graph-error" class="absolute inset-0 flex flex-col items-center justify-center bg-light/80 backdrop-blur-sm hidden p-6">
                <div class="text-center max-w-md">
                    <i class="fa fa-exclamation-triangle text-yellow-500 text-5xl mb-4"></i>
                    <h3 class="text-lg font-medium text-neutral-700 mb-2">数据加载失败</h3>
                    <p class="text-neutral-500 mb-4" id="graph-error-message">无法获取关系数据，请检查网络连接或联系管理员</p>
                    <button id="graph-retry-btn" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors">
                        <i class="fa fa-refresh mr-1"></i> 重试
                    </button>
                </div>
            </div>
        </div>

        <!-- 右侧详情面板 -->
        <aside class="w-72 bg-white border-l border-neutral-200 flex flex-col h-full transform translate-x-full transition-transform duration-300 absolute right-0 top-0 bottom-0 md:static md:translate-x-0 z-10" id="detail-panel">
            <div class="p-4 border-b border-neutral-200 flex justify-between items-center">
                <h2 id="detail-title" class="text-lg font-semibold">人物详情</h2>
                <button id="close-detail" class="md:hidden p-1 hover:bg-neutral-100 rounded-full"><i class="fa fa-times text-neutral-500"></i></button>
            </div>
            <div id="detail-content" class="flex-1 overflow-y-auto p-4">
                <div class="flex flex-col items-center justify-center h-full text-neutral-400">
                    <i class="fa fa-user-circle-o text-5xl mb-3"></i>
                    <p>选择一个人物查看详情</p>
                </div>
            </div>
        </aside>
    </main>

    <!-- 底部状态栏 -->
    <footer class="bg-white border-t border-neutral-200 py-2 px-4 text-sm text-neutral-500 flex justify-between items-center">
        <div><span id="node-count">节点: 0</span> | <span id="link-count">关系: 0</span></div>
        <div>
            <span class="mr-3"><i class="fa fa-info-circle"></i> 点击节点查看详情</span>
            <span><i class="fa fa-arrows"></i> 拖拽可移动图谱</span>
        </div>
    </footer>

    <script>
        // 全局变量
        let graph = { nodes: [], links: [] };
        let simulation;
        let svg, g;
        let transform = d3.zoomIdentity;
        let selectedNode = null;
        let lastFailedRequest = null;

        // ✅ 仅修改这里：使用完整地址，避免相对路径问题
        const API_BASE_URL = 'http://localhost:5000/api';

        // ==== 以下脚本与之前完全一致，无需改动 ====
        const graphSvg = document.getElementById('graph-svg');
        const graphContainer = document.getElementById('graph-container');
        const graphPlaceholder = document.getElementById('graph-placeholder');
        const graphLoading = document.getElementById('graph-loading');
        const graphError = document.getElementById('graph-error');
        const graphErrorMessage = document.getElementById('graph-error-message');
        const graphRetryBtn = document.getElementById('graph-retry-btn');
        const charactersList = document.getElementById('characters-list');
        const characterSearch = document.getElementById('character-search');
        const semanticSearch = document.getElementById('semantic-search');
        const relationDepth = document.getElementById('relation-depth');
        const depthValue = document.getElementById('depth-value');
        const detailPanel = document.getElementById('detail-panel');
        const detailTitle = document.getElementById('detail-title');
        const detailContent = document.getElementById('detail-content');
        const closeDetail = document.getElementById('close-detail');
        const dbStatus = document.getElementById('db-status');
        const nodeCountEl = document.getElementById('node-count');
        const linkCountEl = document.getElementById('link-count');
        const debugPanel = document.getElementById('debug-panel');
        const debugInfoBtn = document.getElementById('debug-info-btn');
        const closeDebug = document.getElementById('close-debug');
        const apiBasePathEl = document.getElementById('api-base-path');
        const lastRequestEl = document.getElementById('last-request');
        const lastResponseEl = document.getElementById('last-response');
        const connectionStatusEl = document.getElementById('connection-status');
        const errorToast = document.getElementById('error-toast');
        const errorTitle = document.getElementById('error-title');
        const errorMessage = document.getElementById('error-message');
        const errorRetry = document.getElementById('error-retry');

        document.addEventListener('DOMContentLoaded', () => {
            apiBasePathEl.textContent = API_BASE_URL;
            initGraph();
            checkHealthStatus();
            loadCharacters();
            setupEventListeners();
        });

        function initGraph() {
            const width = graphContainer.clientWidth;
            const height = graphContainer.clientHeight;
            svg = d3.select('#graph-svg').attr('width', width).attr('height', height);
            svg.call(d3.zoom().extent([[0,0],[width,height]]).scaleExtent([0.3,3])
                .on('zoom', (event) => {
                    transform = event.transform;
                    g.attr('transform', transform);
                }));
            g = svg.append('g');
            simulation = d3.forceSimulation()
                .force('link', d3.forceLink().id(d => d.id).distance(100))
                .force('charge', d3.forceManyBody().strength(-300))
                .force('center', d3.forceCenter(width / 2, height / 2))
                .force('collide', d3.forceCollide().radius(50));
        }

        async function checkHealthStatus() {
            try {
                const url = `${API_BASE_URL}/health`;
                updateDebugInfo('请求', url);
                const res = await fetch(url);
                const data = await res.json();
                updateDebugInfo('响应', `${res.status} - ${JSON.stringify(data)}`);
                if (res.ok && data.status === 'healthy') {
                    dbStatus.innerHTML = '<span class="text-green-500"><i class="fa fa-check-circle"></i> 数据库已连接</span>';
                    connectionStatusEl.textContent = '正常';
                    connectionStatusEl.className = 'font-mono text-green-600';
                } else {
                    throw new Error('服务异常');
                }
            } catch (e) {
                console.error('健康检查失败:', e);
                dbStatus.innerHTML = '<span class="text-red-500"><i class="fa fa-exclamation-circle"></i> 服务不可用</span>';
                connectionStatusEl.textContent = '不可用';
                connectionStatusEl.className = 'font-mono text-red-600';
                showError('服务连接失败', `无法连接到服务器: ${e.message || '未知错误'}`, checkHealthStatus);
            }
        }

        async function loadCharacters(limit = 20) {
            try {
                const url = `${API_BASE_URL}/characters?limit=${limit}`;
                updateDebugInfo('请求', url);
                showLoading();
                const res = await fetch(url);
                const chars = await res.json();
                updateDebugInfo('响应', `${res.status} - ${chars.length} 条记录`);
                if (!res.ok) throw new Error(chars.error || '加载失败');
                renderCharactersList(chars);
            } catch (e) {
                console.error('加载人物列表失败:', e);
                charactersList.innerHTML = `<div class="p-4 text-center"><i class="fa fa-exclamation-circle text-red-500 mb-2"></i><p class="text-red-500 text-sm">加载失败: ${e.message}</p><button class="mt-2 text-xs text-primary hover:underline" onclick="loadCharacters()"><i class="fa fa-refresh mr-1"></i>重试</button></div>`;
                lastFailedRequest = { func: loadCharacters, args: [limit] };
                showError('加载人物列表失败', e.message, () => loadCharacters(limit));
            } finally { hideLoading(); }
        }

        function renderCharactersList(chars) {
            charactersList.innerHTML = '';
            if (chars.length === 0) {
                charactersList.innerHTML = '<div class="p-2 text-neutral-500 text-center">没有找到人物</div>';
                return;
            }
            chars.forEach(c => {
                const div = document.createElement('div');
                div.className = 'px-3 py-2 rounded-md hover:bg-primary/5 cursor-pointer transition-colors text-sm flex justify-between items-center group';
                div.innerHTML = `<span>${c.name}</span><i class="fa fa-arrow-right text-primary/0 group-hover:text-primary/70 transition-colors"></i>`;
                div.onclick = () => loadCharacterRelationships(c.name);
                charactersList.appendChild(div);
            });
        }

        async function loadCharacterRelationships(name) {
            try {
                const depth = relationDepth.value;
                const url = `${API_BASE_URL}/characters/${encodeURIComponent(name)}/relationships?depth=${depth}`;
                updateDebugInfo('请求', url);
                showLoading();
                const res = await fetch(url);
                const data = await res.json();
                updateDebugInfo('响应', `${res.status} - ${JSON.stringify({nodes: data.nodes?.length, links: data.links?.length})}`);
                if (!res.ok) throw new Error(data.error || '加载失败');
                graphError.classList.add('hidden');
                graphPlaceholder.classList.add('hidden');
                graph = data;
                renderGraph();
                nodeCountEl.textContent = `节点: ${data.nodes.length}`;
                linkCountEl.textContent = `关系: ${data.links.length}`;
                selectedNode = name;
            } catch (e) {
                console.error('加载人物关系失败:', e);
                graphError.classList.remove('hidden');
                graphErrorMessage.textContent = e.message;
                lastFailedRequest = { func: loadCharacterRelationships, args: [name] };
                showError('加载关系失败', e.message, () => loadCharacterRelationships(name));
            } finally { hideLoading(); }
        }

        function renderGraph() {
            g.selectAll('*').remove();
            const link = g.append('g').attr('class','links').selectAll('line')
                .data(graph.links).enter().append('line')
                .attr('stroke-width',2).attr('stroke','#999').attr('stroke-opacity',0.6);
            const linkLabel = g.append('g').attr('class','link-labels').selectAll('text')
                .data(graph.links).enter().append('text')
                .text(d => d.type).attr('font-size',10).attr('fill','#666');
            const node = g.append('g').attr('class','nodes').selectAll('g')
                .data(graph.nodes).enter().append('g')
                .call(d3.drag()
                    .on('start', dragstarted)
                    .on('drag', dragged)
                    .on('end', dragended))
                .on('click', nodeClicked);
            node.append('circle')
                .attr('r', d => d.id === selectedNode ? 15 : 12)
                .attr('fill', d => d.id === selectedNode ? '#165DFF' : '#36CFC9')
                .attr('class', 'node-hover');
            node.append('text')
                .attr('dx', 15).attr('dy', '.35em')
                .text(d => d.name).attr('font-size',12).attr('fill','#333').attr('pointer-events','none');
            simulation.nodes(graph.nodes).on('tick', ticked);
            simulation.force('link').links(graph.links);
            simulation.alpha(1).restart();

            function ticked() {
                link.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
                    .attr('x2', d => d.target.x).attr('y2', d => d.target.y);
                linkLabel.attr('x', d => (d.source.x + d.target.x)/2).attr('y', d => (d.source.y + d.target.y)/2);
                node.attr('transform', d => `translate(${d.x},${d.y})`);
            }
        }

        async function loadCharacterDetails(name) {
            try {
                const url = `${API_BASE_URL}/characters/${encodeURIComponent(name)}`;
                updateDebugInfo('请求', url);
                const res = await fetch(url);
                const data = await res.json();
                updateDebugInfo('响应', `${res.status} - 人物详情`);
                if (!res.ok) throw new Error(data.error || '加载失败');
                renderCharacterDetails(data);
            } catch (e) {
                console.error('加载人物详情失败:', e);
                detailContent.innerHTML = `<div class="text-center py-6"><i class="fa fa-exclamation-circle text-red-500 text-2xl mb-2"></i><p class="text-red-500 mb-3">${e.message}</p><button class="text-sm text-primary hover:underline" onclick="loadCharacterDetails('${name}')"><i class="fa fa-refresh mr-1"></i>重试</button></div>`;
                lastFailedRequest = { func: loadCharacterDetails, args: [name] };
                showError('加载详情失败', e.message, () => loadCharacterDetails(name));
            }
        }

        function renderCharacterDetails(data) {
            detailTitle.textContent = data.name;
            detailContent.innerHTML = `
                <div class="text-center mb-4">
                    <div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 text-primary mb-2">
                        <i class="fa fa-user text-2xl"></i>
                    </div>
                    <h3 class="text-xl font-semibold">${data.name}</h3>
                </div>
                <div class="space-y-4">
                    <div class="bg-neutral-100 rounded-lg p-3">
                        <h4 class="text-sm font-medium text-neutral-500 mb-2">关系统计</h4>
                        <div class="flex justify-between items-center">
                            <span>总关系数</span><span class="font-semibold">${data.relationship_count}</span>
                        </div>
                    </div>
                    <div>
                        <h4 class="text-sm font-medium text-neutral-500 mb-2">关系类型</h4>
                        <div class="flex flex-wrap gap-2">
                            ${data.relationship_types.map(t => `<span class="px-2 py-1 bg-primary/10 text-primary text-xs rounded-full">${t}</span>`).join('')}
                        </div>
                    </div>
                    <button class="w-full py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors"
                            onclick="loadCharacterRelationships('${data.name}')">
                        查看${data.name}的关系图谱
                    </button>
                </div>`;
        }

        function showLoading() { graphLoading.classList.remove('hidden'); }
        function hideLoading() { graphLoading.classList.add('hidden'); }

        function showError(title, msg, retryCallback) {
            errorTitle.textContent = title;
            errorMessage.textContent = msg;
            errorToast.classList.remove('hidden');
            errorRetry.onclick = () => { errorToast.classList.add('hidden'); retryCallback(); };
            errorToast.classList.add('error-shake');
            setTimeout(() => errorToast.classList.remove('error-shake'), 500);
            setTimeout(() => errorToast.classList.add('hidden'), 8000);
        }

        function updateDebugInfo(type, content) {
            if (type === '请求') lastRequestEl.textContent = content;
            if (type === '响应') lastResponseEl.textContent = content;
        }

        function dragstarted(event, d) {
            if (!event.active) simulation.alphaTarget(0.3).restart();
            d.fx = d.x; d.fy = d.y;
        }
        function dragged(event, d) {
            d.fx = event.x; d.fy = event.y;
        }
        function dragended(event, d) {
            if (!event.active) simulation.alphaTarget(0);
            d.fx = null; d.fy = null;
        }

        function setupEventListeners() {
            relationDepth.addEventListener('input', () => {
                depthValue.textContent = relationDepth.value;
                if (selectedNode) loadCharacterRelationships(selectedNode);
            });

            characterSearch.addEventListener('input', debounce(async (e) => {
                const q = e.target.value.trim();
                if (q.length < 1) return loadCharacters();
                try {
                    const url = `${API_BASE_URL}/characters/search?q=${encodeURIComponent(q)}`;
                    updateDebugInfo('请求', url);
                    const res = await fetch(url);
                    const chars = await res.json();
                    updateDebugInfo('响应', `${res.status} - ${chars.length} 条记录`);
                    if (!res.ok) throw new Error(chars.error || '搜索失败');
                    renderCharactersList(chars.map(c => ({ name: c.name })));
                } catch (e) {
                    console.error('搜索失败:', e);
                    charactersList.innerHTML = `<div class="p-4 text-center"><i class="fa fa-exclamation-circle text-red-500 mb-2"></i><p class="text-red-500 text-sm">搜索失败: ${e.message}</p><button class="mt-2 text-xs text-primary hover:underline" onclick="document.getElementById('character-search').dispatchEvent(new Event('input'))"><i class="fa fa-refresh mr-1"></i>重试</button></div>`;
                    showError('搜索失败', e.message, () => e.target.dispatchEvent(new Event('input')));
                }
            }, 300));

            semanticSearch.addEventListener('input', debounce(async (e) => {
                const q = e.target.value.trim();
                if (q.length < 2) return;
                try {
                    const url = `${API_BASE_URL}/search_semantic?q=${encodeURIComponent(q)}`;
                    updateDebugInfo('请求', url);
                    const res = await fetch(url);
                    const results = await res.json();
                    updateDebugInfo('响应', `${res.status} - ${results.length} 条记录`);
                    if (!res.ok) throw new Error(results.error || '语义搜索失败');
                    renderCharactersList(results.map(r => ({ name: r.name })));
                } catch (e) {
                    console.error('语义搜索失败:', e);
                    charactersList.innerHTML = `<div class="p-4 text-center"><i class="fa fa-exclamation-circle text-red-500 mb-2"></i><p class="text-red-500 text-sm">语义搜索失败: ${e.message}</p><button class="mt-2 text-xs text-primary hover:underline" onclick="document.getElementById('semantic-search').dispatchEvent(new Event('input'))"><i class="fa fa-refresh mr-1"></i>重试</button></div>`;
                    showError('语义搜索失败', e.message, () => e.target.dispatchEvent(new Event('input')));
                }
            }, 500));

            closeDetail.addEventListener('click', () => detailPanel.classList.add('translate-x-full'));
            graphSvg.onclick = () => { if (window.innerWidth < 768) detailPanel.classList.add('translate-x-full'); };
            window.addEventListener('resize', () => {
                const w = graphContainer.clientWidth, h = graphContainer.clientHeight;
                svg.attr('width', w).attr('height', h);
                simulation.force('center', d3.forceCenter(w/2, h/2)).alpha(0.3).restart();
            });
            graphRetryBtn.addEventListener('click', () => {
                graphError.classList.add('hidden');
                if (lastFailedRequest?.func) lastFailedRequest.func(...lastFailedRequest.args);
                else if (selectedNode) loadCharacterRelationships(selectedNode);
                else loadCharacters();
            });
            debugInfoBtn.addEventListener('click', () => debugPanel.classList.toggle('hidden'));
            closeDebug.addEventListener('click', () => debugPanel.classList.add('hidden'));
        }

        function debounce(fn, wait) {
            let t;
            return function(...args) {
                clearTimeout(t);
                t = setTimeout(() => fn.apply(this, args), wait);
            };
        }
    </script>
</body>
</html>